# 해시코드와 이퀄

# 객체 비교(equals()와 ==)

equals
equals()는 매개변수로 들어오는 객체와 자신의 객체가 같은지 비교하는 기능을 합니다. 이 메소드의 내부를 보면 연산자인 == 과 동일한 결과를 리턴합니다. 오로지 참조값(객체의 주소값)이 같은지 확인하는 것입니다.

자바에서는 두 객체를 동등비교 할때 equals() 메소드를 흔히 사용합니다. equals()메소드는 두 객체를 비교해서 논리적으로 동등하면 true를 리턴하고 그렇지 않으면 false를 리턴해줍니다. 여기서 논리적으로 동등하다는 것은 둘의 참조값이 다르더라도 객체 내부 value는 같다는 것을 의미합니다.

equals()를 재정의한 대표적인 예로는 String class가 있습니다. String class는 equals() 메소드를 재정의해서 번지비교가 아닌 문자열값을 비교합니다.

정리하자면,

  • 동일성 비교는 == 비교로서, 객체 인스턴스의 주소 값을 비교하는 것입니다.
  • 등등성 비교는 equals()를 사용해서 객체 내부의 값을 비교하는 것입니다.
  • primitive data type의 경우는 == 를 통해 값 비교가 가능합니다.

# primitive 타입이 == 비교를 통해 값 비교가 가능한 이유

변수 선언부는 Java Runtime Data Area의 Stack 영역에 저장이 되고, 해당 변수에 저장된 상수는 Runtime Constant Pool에 저장되어있습니다. Stack의 변수 선언부는 해당 Runtime Contant Pool의 주소값을 가지게 되고, 만약 다른 변수도 같은 상수를 저장하고 있다면, 이 다른 변수도 같은 Runtime Contant Pool의 주소값을 가지게 때문에 엄밀히 말하면 primitive type 역시 주소값 비교가 되는 것입니다.

# equals의 재정의

equals는 객체의 값이 일치한지 비교하기 위해 사용한다고 했습니다. 그렇다면 예제로 Car 클래스의 equals를 재정의 해보겠습니다.

예제Car클래스
Car클래스에 equals만 재정의 해두었습니다.

car비교1
Car 객체의 name이 같은 car1, car2 객체는 논리적으로 같은 객체로 판단되었습니다.

car비교2
Car 객체 2개를 List cars에 넣어주었으니 결과는 2가 나올것입니다.

만약, Collection에 중복되지 않는 Car 객체만 넣으라는 요구사항이 추가되었다고 가정해봅시다.
요구사항을 만족시키기 위해 List 대신 Set을 사용했습니다.

car비교3
추가된 두 Car객체의 이름이 같아서 논리적으로 같은 객체라 판단하고 size가 1이 나올 것으로 예상했지만, 결과는 2가 나왔습니다.

이는 haseCode를 equals와 함께 재정의하지 않아서 발생한 문제입니다.

# haseCode

객체 해시코드란 객체를 식별하는 하나의 정수값을 말합니다. Object의 hashCode() 메소드는 객체의 메모리 번지를 이용해서 해시코드를 만들어 리턴하기 때문에 객체 마다 다른 값을 가지고 있습니다.

객체의 값을 동등성 비교시 hashCode()를 오버라이딩할 필요성이 있는데, 컬렉션 프레임워크에서 HashSet, HashMap, HashTable은 객체가 논리적으로 같은지 비교할 때 다음 과정을 거칩니다.

논리적비교단계
hashCode() 메소드를 실행해서 리턴된 해시코드 값이 같은 지 확인합니다. 해시 코드 값이 다르면 다른 객체로 판단하고 해시 코드 값이 같으면 equals()로 다시 비교합니다. 이렇게 두 메소드가 모두 true 가 나오면 동등객체로 판단합니다. 즉, 해시코드 값이 다른 엔트리끼리는 동치성 비교를 시도조차 하지 않습니다.

# Java HashTable

equals와 hashcode 메서드를 이해하기 위해서 자바에서 HashTable이 작동하는 원리를 간단히 살펴보겠습니다.
(HashTable 뿐만 아니라 HashMap, HashSet 모두 아래의 동작 원리와 동일합니다.)

HashTable은 <key,value> 형태로 데이터를 저장합니다.
이 때 해시 함수(Hash Function)을 이용하여 key값을 기준으로 고유한 식별값인 해시값을 만듭니다. (hashcode가 해시값을 만드는 역할을 합니다.)
그리고 이 해시값을 버킷(Bucket)에 저장합니다.

하지만 HashTable 크기는 한정적이기 때문에 같은 서로 다른 객체라 하더라도 같은 해시값을 갖게 될 수도 있습니다.
이것을 **해시 충돌(Hash Collisions)*이라고 합니다.
이런 경우 아래와 같이 해당 버킷(Bucket)에 LinkedList 형태로 객체를 추가합니다.
(
참고로 java8인가 9버전부터 LinkedList 아이템의 갯수가 8개 이상으로 넘어가면 TreeMap 자료구조로 저장된다고 합니다.)

이처럼 같은 해시값의 버킷 안에 다른 객체가 있는 경우 equals 메서드가 사용됩니다.

HashTable에 put 메서드로 객체를 추가하는 경우

  • 값이 같은 객체가 이미 있다면(equals()가 true) 기존 객체를 덮어쓴다.
  • 값이 같은 객체가 없다면(equals()가 false) 해당 entry를 LinkedList에 추가한다.

HashTable에 get 메서드로 객체를 조회하는 경우

  • 값이 같은 객체가 있다면 (equals()가 true) 그 객체를 리턴한다.
  • 값이 같은 객체가 없다면(equals()가 false) null을 리턴한다.

해시테이블
위 그림에서 세 객체 (Entry<K1,V1>, Entry<K2,V2>, Entry<K3,V3>)는 서로 같은 해시값을 갖습니다. 따라서 hashcode() 메서드는 같은 값을 리턴합니다. 하지만 서로 값이 다른 객체이기 때문에 equals() 메서드는 false를 리턴합니다.

# haseCode 재정의

앞서 봤던 예제에서는 Car 클래스의 hashCode 메소드가 재정의 되지 않아서, Object 클래스의 hashCode 메서드가 사용되었습니다.
Object 클래스의 haseCode 메서드는 객체의 고유한 주소 값을 int 값으로 변환하기 때문에 객체마다 다른 값을 리턴합니다. 따라서 두 개의 Car 객체는 서로 다른 haseCode 메서드의 리턴 값으로 인해 다른 객체로 판단된 것입니다.

그러면 문제를 해결하기 위해 Car 클래스에 hashCode 매서드를 재정의 해보겠습니다.

Car_hasecode
intellij 의 Generate 기능을 사용했더니 Objects.hash 메서드를 호출하는 로직으로 hashCode 메서드가 재정의 됐습니다. Objects.hash 메서드는 hashCode 메서드를 재정의하기 위해 간편히 사용할 수 있는 메서드이지만 속도가 느립니다. 인자를 담기 위한 배열이 만들어지고 인자 중 기본 타입이 있다면 박싱과 언박싱도 거쳐야 하기 때문입니다.

성능에 아주 민감하지 않은 대부분의 프로그램은 간편하게 Objects.hash 메서드를 사용해서 hashCode 메서드를 재정의해도 문제 없습니다. 민감한 경우에는 직접 재정의해주는 게 좋은데, 관련 정보는 Guide to hashCode() in Java (opens new window) 이 글을 참고하시면 됩니다.

# 정리

hashcode()를 재정의 하지 않으면 같은 값 객체라도 해시값이 다를 수 있습니다. 따라서 HashTable에서 해당 객체가 저장된 버킷을 찾을 수 없는 경우가 발생합니다.
반대로 equals()를 재정의하지 않으면 hashcode()가 만든 해시값을 이용해 객체가 저장된 버킷을 찾을 수는 있지만 해당 객체가 자신과 같은 객체인지 값을 비교할 수 없기 때문에 null을 리턴하게 됩니다. 따라서 역시 원하는 객체를 찾을 수 없습니다.

이러한 이유로 객체의 정확한 동등 비교를 위해서는 (특히 Hash 관련 컬렉션 프레임워크를 사용할때!) Object의 equals() 메소드만 재정의하지 말고 hashCode()메소드도 재정의해서 논리적 동등 객체일경우 동일한 해시코드가 리턴되도록 해야합니다.

# 참고자료